import { IHttpMigrateRoute, OpenApi } from "@samchon/openapi";
import ts from "typescript";
import { ExpressionFactory } from "typia/lib/factories/ExpressionFactory";
import { IdentifierFactory } from "typia/lib/factories/IdentifierFactory";
import { LiteralFactory } from "typia/lib/factories/LiteralFactory";
import { TypeFactory } from "typia/lib/factories/TypeFactory";

import { INestiaMigrateConfig } from "../structures/INestiaMigrateConfig";
import { INestiaMigrateController } from "../structures/INestiaMigrateController";
import { FilePrinter } from "../utils/FilePrinter";
import { StringUtil } from "../utils/StringUtil";
import { NestiaMigrateImportProgrammer } from "./NestiaMigrateImportProgrammer";
import { NestiaMigrateSchemaProgrammer } from "./NestiaMigrateSchemaProgrammer";

export namespace NestiaMigrateNestMethodProgrammer {
  export interface IContext {
    config: INestiaMigrateConfig;
    components: OpenApi.IComponents;
    importer: NestiaMigrateImportProgrammer;
    controller: INestiaMigrateController;
    route: IHttpMigrateRoute;
  }

  export const write = (ctx: IContext): ts.MethodDeclaration => {
    const output: ts.TypeNode = ctx.route.success
      ? NestiaMigrateSchemaProgrammer.write({
          components: ctx.components,
          importer: ctx.importer,
          schema: ctx.route.success.schema,
        })
      : TypeFactory.keyword("void");

    const method: ts.MethodDeclaration = ts.factory.createMethodDeclaration(
      [
        ...writeMethodDecorators(ctx),
        ts.factory.createToken(ts.SyntaxKind.PublicKeyword),
        ts.factory.createToken(ts.SyntaxKind.AsyncKeyword),
      ],
      undefined,
      ctx.route.accessor.at(-1)!,
      undefined,
      undefined,
      writeParameters(ctx),
      ts.factory.createTypeReferenceNode("Promise", [output]),
      ts.factory.createBlock(
        [
          ...[
            ...ctx.route.parameters.map((p) => p.key),
            ...(ctx.route.headers ? ["headers"] : []),
            ...(ctx.route.query ? ["query"] : []),
            ...(ctx.route.body ? ["body"] : []),
          ].map((str) =>
            ts.factory.createExpressionStatement(
              ts.factory.createIdentifier(str),
            ),
          ),
          ts.factory.createReturnStatement(
            ts.factory.createCallExpression(
              IdentifierFactory.access(
                ts.factory.createIdentifier(
                  ctx.importer.external({
                    type: "default",
                    library: "typia",
                    name: "typia",
                  }),
                ),
                "random",
              ),
              [output],
              undefined,
            ),
          ),
        ],
        true,
      ),
    );
    return FilePrinter.description(
      method,
      writeDescription(ctx.config, ctx.route),
    );
  };

  const writeDescription = (
    config: INestiaMigrateConfig,
    method: IHttpMigrateRoute,
  ): string =>
    [
      method.comment(),
      `@${config.author?.tag ?? "nestia"} ${config.author?.value ?? "Generated by Nestia - https://github.com/samchon/nestia"}`,
    ].join("\n");

  const writeMethodDecorators = (ctx: IContext): ts.Decorator[] => {
    const external = (lib: string, instance: string): ts.Identifier =>
      ts.factory.createIdentifier(
        ctx.importer.external({
          type: "instance",
          library: lib,
          name: instance,
        }),
      );

    // EXAMPLES
    const decorators: ts.Decorator[] = [];
    if (ctx.route.success)
      decorators.push(
        ...writeExampleDecorators("Response")(ctx.importer)(
          ctx.route.success.media(),
        ),
      );

    // HUMAN-ONLY
    if (ctx.route.operation()["x-samchon-human"] === true)
      decorators.push(
        ts.factory.createDecorator(
          ts.factory.createCallExpression(
            external("@nestia/core", "HumanRoute"),
            undefined,
            undefined,
          ),
        ),
      );

    // ROUTER
    const localPath: string = ctx.route.emendedPath
      .slice(ctx.controller.path.length)
      .split("/")
      .filter((str) => !!str.length)
      .join("/");
    const router = (instance: string) =>
      ts.factory.createDecorator(
        ts.factory.createCallExpression(
          IdentifierFactory.access(
            external("@nestia/core", instance),
            StringUtil.capitalize(ctx.route.method),
          ),
          [],
          localPath.length
            ? [ts.factory.createStringLiteral(localPath)]
            : undefined,
        ),
      );
    if (ctx.route.success?.["x-nestia-encrypted"])
      decorators.push(router("EncryptedRoute"));
    else if (ctx.route.success?.type === "text/plain")
      decorators.push(
        ts.factory.createDecorator(
          ts.factory.createCallExpression(
            external("@nestjs/common", StringUtil.capitalize(ctx.route.method)),
            [],
            [ts.factory.createStringLiteral(ctx.route.path)],
          ),
        ),
      );
    else if (ctx.route.success?.type === "application/x-www-form-urlencoded")
      decorators.push(router("TypedQuery"));
    else if (ctx.route.method === "head")
      decorators.push(
        ts.factory.createDecorator(
          ts.factory.createCallExpression(
            external("@nestjs/common", "Head"),
            [],
            [ts.factory.createStringLiteral(ctx.route.path)],
          ),
        ),
      );
    else if (
      ctx.route.success === null ||
      ctx.route.success?.type === "application/json"
    )
      decorators.push(router("TypedRoute"));
    for (const [key, value] of Object.entries(ctx.route.exceptions ?? {}))
      decorators.push(
        ts.factory.createDecorator(
          ts.factory.createCallExpression(
            external("@nestia/core", "TypedException"),
            [
              NestiaMigrateSchemaProgrammer.write({
                components: ctx.components,
                importer: ctx.importer,
                schema: value.schema,
              }),
            ],
            [
              isNaN(Number(key))
                ? ts.factory.createStringLiteral(key)
                : ExpressionFactory.number(Number(key)),
              ...(value.response().description?.length
                ? [
                    ts.factory.createStringLiteral(
                      value.response().description!,
                    ),
                  ]
                : []),
            ],
          ),
        ),
      );
    return decorators;
  };

  const writeParameters = (ctx: IContext): ts.ParameterDeclaration[] => [
    ...ctx.route.parameters.map((p) =>
      ts.factory.createParameterDeclaration(
        [
          ...writeExampleDecorators("Parameter")(ctx.importer)(p.parameter()),
          ts.factory.createDecorator(
            ts.factory.createCallExpression(
              ts.factory.createIdentifier(
                ctx.importer.external({
                  type: "instance",
                  library: "@nestia/core",
                  name: "TypedParam",
                }),
              ),
              undefined,
              [ts.factory.createStringLiteral(p.key)],
            ),
          ),
        ],
        undefined,
        p.key,
        undefined,
        NestiaMigrateSchemaProgrammer.write({
          components: ctx.components,
          importer: ctx.importer,
          schema: p.schema,
        }),
      ),
    ),
    ...(ctx.route.headers
      ? [
          writeDtoParameter({
            method: "TypedHeaders",
            variable: "headers",
            arguments: [],
          })(ctx.components)(ctx.importer)({
            required: true,
            schema: ctx.route.headers.schema,
            example: ctx.route.headers.example(),
            examples: ctx.route.headers.examples(),
          }),
        ]
      : []),
    ...(ctx.route.query
      ? [
          writeDtoParameter({
            method: "TypedQuery",
            variable: "query",
            arguments: [],
          })(ctx.components)(ctx.importer)({
            required: true,
            schema: ctx.route.query.schema,
            example: ctx.route.query.example(),
            examples: ctx.route.query.examples(),
          }),
        ]
      : []),
    ...(ctx.route.body
      ? [
          writeDtoParameter({
            method: ctx.route.body["x-nestia-encrypted"]
              ? "EncryptedBody"
              : ctx.route.body.type === "application/json"
                ? "TypedBody"
                : ctx.route.body.type === "application/x-www-form-urlencoded"
                  ? ["TypedQuery", "Body"]
                  : ctx.route.body.type === "text/plain"
                    ? "PlainBody"
                    : ctx.route.body.type === "multipart/form-data"
                      ? ["TypedFormData", "Body"]
                      : "TypedBody",
            variable: "body",
            arguments:
              ctx.route.body.type === "multipart/form-data"
                ? [
                    ts.factory.createArrowFunction(
                      undefined,
                      undefined,
                      [],
                      undefined,
                      undefined,
                      ts.factory.createCallExpression(
                        ts.factory.createIdentifier(
                          ctx.importer.external({
                            type: "default",
                            library: "multer",
                            name: "Multer",
                          }),
                        ),
                        undefined,
                        undefined,
                      ),
                    ),
                  ]
                : [],
          })(ctx.components)(ctx.importer)({
            schema: ctx.route.body.schema,
            required: !(
              (ctx.route.body.type === "application/json" ||
                ctx.route.body.type === "text/plain") &&
              ctx.route.operation().requestBody?.required === false
            ),
            example: ctx.route.body.media().example,
            examples: ctx.route.body.media().examples,
          }),
        ]
      : []),
  ];

  const writeDtoParameter =
    (accessor: {
      method: string | [string, string];
      variable: string;
      arguments: ts.Expression[];
    }) =>
    (components: OpenApi.IComponents) =>
    (importer: NestiaMigrateImportProgrammer) =>
    (props: {
      schema: OpenApi.IJsonSchema;
      required: boolean;
      example?: any;
      examples?: Record<string, any>;
    }): ts.ParameterDeclaration => {
      const instance = ts.factory.createIdentifier(
        importer.external({
          type: "instance",
          library: "@nestia/core",
          name:
            typeof accessor.method === "string"
              ? accessor.method
              : accessor.method[0],
        }),
      );
      return ts.factory.createParameterDeclaration(
        [
          ...writeExampleDecorators("Parameter")(importer)(props),
          ts.factory.createDecorator(
            ts.factory.createCallExpression(
              typeof accessor.method === "string"
                ? instance
                : IdentifierFactory.access(instance, accessor.method[1]),
              undefined,
              accessor.arguments,
            ),
          ),
        ],
        undefined,
        accessor.variable,
        props.required === false
          ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
          : undefined,
        NestiaMigrateSchemaProgrammer.write({
          components,
          importer,
          schema: props.schema,
        }),
      );
    };

  const writeExampleDecorators =
    (kind: "Response" | "Parameter") =>
    (importer: NestiaMigrateImportProgrammer) =>
    (media: {
      example?: any;
      examples?: Record<string, any>;
    }): ts.Decorator[] => [
      ...(media.example !== undefined
        ? [
            ts.factory.createDecorator(
              ts.factory.createCallExpression(
                IdentifierFactory.access(
                  ts.factory.createIdentifier(
                    importer.external({
                      type: "instance",
                      library: "@nestia/core",
                      name: "SwaggerExample",
                    }),
                  ),
                  kind,
                ),
                [],
                [LiteralFactory.write(media.example)],
              ),
            ),
          ]
        : []),
      ...Object.entries(media.examples ?? {}).map(([key, value]) =>
        ts.factory.createDecorator(
          ts.factory.createCallExpression(
            IdentifierFactory.access(
              ts.factory.createIdentifier(
                importer.external({
                  type: "instance",
                  library: "@nestia/core",
                  name: "SwaggerExample",
                }),
              ),
              kind,
            ),
            [],
            [ts.factory.createStringLiteral(key), LiteralFactory.write(value)],
          ),
        ),
      ),
    ];
}
